/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ThumbsDown.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");

Cu.import("resource://thumbsdown/ScriptStore.jsm");
Cu.import("resource://thumbsdown/ThumbsDown.jsm");
Cu.import("resource://thumbsdown/util.jsm");

const TEMPLATE_PATH = "chrome://thumbsdown/content/templates/";
const TEMPLATE_NAMES =
[
    "getElementById",
    "querySelector",
    "xpath",
    "RegExp",
    "Redirect",
];


const dialog =
{
    STRING_BUNDLE_URL: "chrome://thumbsdown/locale/script.properties",
    
    get dirty() { return this._dirty; },
    set dirty(dirty)
    {
        if (dirty)
            dirty = this._isScriptModified();
        
        document.documentElement.getButton("accept").disabled = (! dirty);
        this._dirty = dirty;
    },
    
    get operation() { return this._operation; },
    set operation(operation)
    {
        this._operation = operation;
        this._updateDialogTitle();
    },
    
    // sqlite ids are always >0 (unless manually set to something else)
    get scriptId() { return this._scriptId; },
    set scriptId(scriptId)
    {
        this._scriptId = scriptId;
        
        if (scriptId)
            document.documentElement.setAttribute("windowtype", "thumbsdown_scriptDialog_" + scriptId);
        else
            document.documentElement.removeAttribute("windowtype");
    },
    
    onLoad: function()
    {
        let {operation, scriptId, record} = window.arguments[0].wrappedJSObject;
        
        let self = this;
        ScriptStore.open();
        
        this._ = getStringBundleGetter(Services.strings.createBundle(this.STRING_BUNDLE_URL));
        
        this._operation = operation;
        
        if (this._operation == "set")
            if (scriptId)
                this.scriptId = scriptId;
            else
                throw new Error("operation: 'set'; invalid scriptId: " + scriptId);
        
        if (scriptId)
            record = ScriptStore.getScript(scriptId)._record;
        
        this.$name = document.getElementById("tbox_name");
        this.$enabled = document.getElementById("btn_enabled");
        this.$thumbnailType = document.getElementById("mlist_thumbnailType");
        this.$thumbnailPattern = document.getElementById("tbox_thumbnailPattern");
        this.$linkType = document.getElementById("mlist_linkType");
        this.$linkExpr = document.getElementById("tbox_linkExpr");
        this.$linkPattern = document.getElementById("tbox_linkPattern");
        this.$type = document.getElementById("mlist_type");
        this.$regexpSource = document.getElementById("radio_regexp_source");
        this.$regexpPattern = document.getElementById("tbox_regexp_pattern");
        this.$regexpTemplate = document.getElementById("tbox_regexp_template");
        this.$customCode = document.getElementById("tbox_custom_code");
        this.$wrappedAttrName = document.getElementById("tbox_wrapped_attr_name");
        this.$wrappedAttrValue = document.getElementById("tbox_wrapped_attr_value");
        this.$typeError = document.getElementById("tip_typeError");
        
        this.currentType = this.$type.value;
        
        if (! record)
        {
            this.$linkType.value = "normal";
        }
        else
        {
            this.$name.value = record.name || "";
            this.$enabled.value = (! record.disabled);
            this.$thumbnailType.value = record.thumbnailType || "img";
            this.$thumbnailPattern.value = record.thumbnailPattern || "";
            this.$linkType.value = record.linkType || "normal";
            this.$linkExpr.value = record.linkExpr || "";
            this.$linkPattern.value = record.linkPattern || "";
            this.$type.value = record.type || "direct";
            
            switch (record.type)
            {
                case "regexp":
                    this.$regexpSource.value = record.subType || "thumbnail";
                    this.$regexpPattern.value = record.action1 || "";
                    this.$regexpTemplate.value = record.action2 || "";
                    break;
                case "custom":
                    this.$customCode.value = record.action1 || "";
                    break;
                case "wrapped":
                    this.$wrappedAttrName.value = record.action1 || "";
                    this.$wrappedAttrValue.value = record.action2 || "";
                    break;
            }
            
            this.currentType = record.type;
            this._showTypeElements(this.currentType, true)
        }
        
        this._updateDialogTitle();
        this._updateTypeElements();
        this._updateIdentifyElements();
        
        // label column is locale specific
        for each (let elem in NodeIterator(document.querySelectorAll("column.label")))
            elem.style.width = this._("css.labelColumn.width");
        
        // populate custom code template dropdown
        for each (let name in TEMPLATE_NAMES)
        {
            let menuItem = createElement(document, NS_XUL, "menuitem", {label: name, oncommand: "dialog.loadScriptTemplate(this.label)"});
            document.getElementById("mpopup_custom_template").appendChild(menuItem);
        }
        
        // dirty init & listeners
        this.dirty = (this.operation == "add");
        for each (let elem in NodeIterator(document.querySelectorAll("textbox")))
            elem.addEventListener("input", function() { self.dirty = true; }, false);
        for each (let elem in NodeIterator(document.querySelectorAll("button,menulist,radiogroup")))
            elem.addEventListener("command", function() { self.dirty = true; }, false);
        
        // observe extenal ScriptStore changes
        Services.obs.addObserver(this, ScriptStore.TOPIC, false);
        
        this.initialized = true;
    },
    
    onUnload: function()
    {
        Services.obs.removeObserver(this, ScriptStore.TOPIC);
    },
    
    // nsIObserver
    observe: function(subject, topic, data)
    {
        // change is local
        if (this._storeInProgress)
            return;
        
        if (! this.scriptId)
            return;
        
        data = JSON.parse(data);
        
        switch (data.type)
        {
            case ScriptStore.TOPIC_TYPE_MODIFIED:
            {
                // script is modified externally -> mark this dialog dirty
                if (data.ids.indexOf(this.scriptId) != -1)
                    this.dirty = true;
                return;
            }
            
            case ScriptStore.TOPIC_TYPE_REMOVED:
            {
                // script is removed externally -> mark this dialog dirty and remove scriptId
                if (data.ids.indexOf(this.scriptId) != -1)
                {
                    this.scriptId = null;
                    this.dirty = true;
                }
                return;
            }
        }
    },
    
    onNameInput: function()
    {
        if (this._nameInputTimer)
            this._nameInputTimer.cancel();
        
        this._nameInputTimer = callLater(500, this, this._updateDialogTitle);
    },
    
    onThumbnailTypeSelect: function()
    {
        this._updateTypeElements();
        this._updateIdentifyElements();
    },
    
    onLinkTypeSelect: function()
    {
        this._updateTypeElements();
        this._updateIdentifyElements();
    },
    
    onTypeSelect: function()
    {
        this._updateTypeElements();
        this._showTypeElements(this.currentType, false);
        this.currentType = this.$type.value;
        this._showTypeElements(this.currentType, true);
    },
    
    onRegexpSourceSelect: function()
    {
        if (! this.initialized)
            return;
        
        this._updateTypeElements();
    },
    
    loadScriptTemplate: function(templateName)
    {
        let scriptTemplate = loadContent(TEMPLATE_PATH + templateName + ".js");
        this.$customCode.value = scriptTemplate;
    },
    
    _toRecord: function()
    {
        let record = {}
        record.name = this.$name.value.trim();
        record.disabled = (! this.$enabled.value);
        record.type = this.$type.value;
        record.thumbnailType = this.$thumbnailType.value;
        record.thumbnailPattern = this.$thumbnailPattern.value.trim();
        record.linkType = this.$linkType.value;
        record.linkExpr = (this.$linkExpr.disabled ? "" : this.$linkExpr.value.trim());
        record.linkPattern = (this.$linkPattern.disabled ? "" : this.$linkPattern.value.trim());
        
        // validate info fields
        if (record.name.length == 0) throw this._("error.empty", this._("name"));
        
        // validate type combination
        let typeError = this._validateTypes();
        if (typeError) throw this._("error.illegalTypes") + "\n" + typeError;
        
        // validate identify fields
        if ((record.linkType == "xpath") && (record.linkExpr.length == 0))
            throw this._("error.empty", this._("linkExpr"));
        if ((record.thumbnailPattern.length == 0) && (record.linkPattern.length == 0))
            throw this._("error.empty", this._("identifyPattern"));
        if (! this._toRegExp(record.thumbnailPattern))
            throw this._("error.noRegExp", this._("thumbnailPattern"));
        if (! this._toRegExp(record.linkPattern))
            throw this._("error.noRegExp", this._("linkPattern"));
        
        // validate resolve fields
        switch (record.type)
        {
            case "regexp":
                record.subType = this.$regexpSource.value;
                record.action1 = this.$regexpPattern.value.trim();
                record.action2 = this.$regexpTemplate.value.trim();
                if (record.action1.length == 0) throw this._("error.empty", this._("regexp.pattern"));
                if (record.action2.length == 0) throw this._("error.empty", this._("regexp.template"));
                if (! this._toRegExp(record.action1)) throw this._("error.noRegExp", this._("regexp.pattern"));
                break;
            
            case "custom":
                record.action1 = this.$customCode.value.trim();
                if (record.action1.length == 0) throw this._("error.empty", this._("custom.code"));
                break;
            
            case "wrapped":
                record.action1 = this.$wrappedAttrName.value.trim();
                record.action2 = this.$wrappedAttrValue.value.trim();
                if (record.action1.length == 0) throw this._("error.empty", this._("wrapped.attr.name"));
                if (record.action2.length == 0) throw this._("error.empty", this._("wrapped.attr.value"));
                if (! this._toRegExp(record.action2)) throw this._("error.noRegExp", this._("wrapped.attr.value"));
                break;
        }
        
        // convert empty fields ("") to null
        record.thumbnailPattern = record.thumbnailPattern || null;
        record.linkExpr = record.linkExpr || null;
        record.linkPattern = record.linkPattern || null;
        
        return record;
    },
    
    _validateTypes: function()
    {
        if ((this.$thumbnailType.value == "css-background-image") && (this.$linkType.value == "normal"))
            return this._("error.illegalTypes1", this._("linkType.normal"), this._("thumbnailType.css-background-image"));
        if ((this.$linkType.value == "none") && (this.$type.value != "regexp"))
            return this._("error.illegalTypes2", this._("type." + this.$type.value), this._("linkType.none"));
        if ((this.$linkType.value == "none") && (this.$type.value == "regexp") && (this.$regexpSource.value == "link"))
            return this._("error.illegalTypes3", this._("type.regexp.link"), this._("linkType.none"));
        
        return null;
    },
    
    _isScriptModified: function(contentOnly)
    {
        try
        {
            if (! this.scriptId)
                return true;
            
            let record1 = this._toRecord();
            let record2 = ScriptStore.getScript(this.scriptId)._record;
            
            let props = "name type subType thumbnailType thumbnailPattern linkType linkExpr linkPattern action1 action2".split(" ");
            if (! contentOnly)
                props.push("disabled");
            
            for each (let prop in props)
                if (record1[prop] != record2[prop])
                    return true;
            
            return false;
        }
        catch (e)
        {
            return true;
        }
    },
    
    copyToClipboard: function()
    {
        try
        {
            let record = this._toRecord();
            ThumbsDown.copyScriptsToClipboard([record]);
        }
        catch (e if (typeof(e) == "string"))
        {
            Services.prompt.alert(window, this._("error.title"), e);
        }
    },
    
    onDialogAccept: function()
    {
        this._storeInProgress = true;
        
        try
        {
            let record = this._toRecord();
            
            if (this.scriptId)
            {
                record.id = this.scriptId;
                if (this._isScriptModified(true))
                    ScriptStore.setScript(record);
                else
                    ScriptStore.setScriptDisabled(record.id, record.disabled);
            }
            else
            {
                let script = ScriptStore.addScript(record);
                this.scriptId = script.id;
            }
            
            this.dirty = false;
        }
        catch (e if (typeof(e) == "string"))
        {
            Services.prompt.alert(window, this._("error.title"), e);
        }
        finally
        {
            this._storeInProgress = false;
        }
        
        return false;
    },
    
    onDialogCancel: function()
    {
        if (this.dirty)
        {
            // cancel have to be button #1 - https://bugzilla.mozilla.org/show_bug.cgi?id=345067
            let title = this._("close.confirm.title");
            let msg = this._("close.confirm.msg");
            let button0Label = this._("close.confirm.btn.close");
            let flags =
                Ci.nsIPromptService.BUTTON_POS_0 * Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
                Ci.nsIPromptService.BUTTON_POS_1 * Ci.nsIPromptService.BUTTON_TITLE_CANCEL + 
                Ci.nsIPromptService.BUTTON_POS_1_DEFAULT;
            
            let result = Services.prompt.confirmEx(window, title, msg, flags, button0Label, null, null, null, {});
            if (result)
                return false;
        }
        
        return true;
    },
    
    _updateDialogTitle: function()
    {
        let name = this.$name.value.trim();
        let title = name + (name.length ? " - " : "") + this._("dialogTitle." + this.operation) + " - ThumbsDown";
        document.title = title;
    },
    
    _updateTypeElements: function()
    {
        let typeError = this._validateTypes();
        if (typeError)
        {
            this.$typeError.label = this._("error.illegalTypes") + "\n" + typeError;
            document.documentElement.classList.add("type-error");
        }
        else
            document.documentElement.classList.remove("type-error");
    },
    
    _updateIdentifyElements: function()
    {
        // selected link type provides link
        let hasLink = (["normal", "xpath"].indexOf(this.$linkType.value) != -1);
        // selected link type requires link expression
        let hasLinkExpr = (["xpath"].indexOf(this.$linkType.value) != -1);
        
        this.$linkExpr.disabled = (! hasLinkExpr);
        this.$linkPattern.disabled = (! hasLink);
    },
    
    _showTypeElements: function(type, show)
    {
        for each (let elem in NodeIterator(document.querySelectorAll(".type_" + type)))
            elem.hidden = (! show);
        if (show)
            window.sizeToContent();
    },
    
    _toRegExp: function(pattern)
    {
        try { return new RegExp("^" + pattern + "$"); }
        catch (e) { return null; }
    }
}
